<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文本转语音</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background-color: #f4f4f4;
            margin: 0;
            padding: 20px;
        }
        .container {
            background-color: #fff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            width: 100%;
            max-width: 600px; /* Increase max-width slightly */
            text-align: center;
        }
        .config-item {
            display: flex;
            gap: 10px; /* Increase gap between label and input */
            width: calc(50% - 10px); /* Adjust width for two items per row, considering gap */
            max-width: 250px; /* Limit max width for larger screens, adjusted for new layout */
        }
        .config-item label {
            text-align: right; /* Align label text to the right */
            flex-shrink: 0; /* Prevent label from shrinking */
            width: 45px; /* Fixed width for labels */
        }
        .config-item input[type="range"],
        .config-item select {
            width: 100%;
        }
        textarea {
            width: calc(100% - 20px);
            height: 150px;
            margin-bottom: 15px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            resize: vertical;
            font-size: 16px;
        }
        button {
            background-color: #007bff;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }
        button:hover {
            background-color: #0056b3;
        }
        audio {
            width: 100%;
            margin-top: 20px;
        }
        .message {
            margin-top: 15px;
            color: green;
        }
        .error {
            color: red;
        }
        .subtitle-container {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            min-height: 50px;
            max-height: 150px; /* Limit height for scrolling */
            overflow-y: auto;
            text-align: left;
            font-size: 1.1em;
            line-height: 1.5;
            background-color: #f9f9f9;
        }
        .subtitle-line {
            padding: 2px 0;
            color: #555;
        }
        .subtitle-line.highlight {
            background-color: #e0e0e0;
            color: #000;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Edge TTS 文本转语音</h1>
        <textarea id="textInput" placeholder="请输入要转换的文本..."> 
            这是一个使用edge-tts在线文本转语音的示例。你可以输入任意文本，选择语音和语速，
            然后点击生成语音按钮，即可听到转换后的语音。
        </textarea>
        <div style="margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 20px;">
            <div class="config-item">
                <label for="voiceSelect">语音:</label>
                <select id="voiceSelect"></select>
            </div>
            <div class="config-item">
                <label for="rateInput">语速:</label>
                <input type="range" id="rateInput" min="-100" max="100" value="0" oninput="this.nextElementSibling.value = this.value + '%'">
                <output>0%</output>
            </div>
            <div class="config-item">
                <label for="pitchInput">音高:</label>
                <input type="range" id="pitchInput" min="-50" max="50" value="0" oninput="this.nextElementSibling.value = this.value + 'Hz'">
                <output>0Hz</output>
            </div>
            <div class="config-item">
                <label for="volumeInput">音量:</label>
                <input type="range" id="volumeInput" min="0" max="100" value="100" oninput="this.nextElementSibling.value = this.value + '%'">
                <output>100%</output>
            </div>
            <div class="config-item">
                <label for="generateSubtitles">字幕:</label>
                <input type="checkbox" id="generateSubtitles" checked>
            </div>
        </div>
        <button onclick="synthesize()">生成语音</button>
        <audio id="audioPlayer" controls autoplay></audio>
        <div id="subtitleContainer" class="subtitle-container"></div>
        <div id="message" class="message"></div>
    </div>

    <script>
        let currentSubtitles = [];
        let currentSubtitleIndex = -1;

        async function synthesize() {
            const textInput = document.getElementById('textInput');
            const audioPlayer = document.getElementById('audioPlayer');
            const messageDiv = document.getElementById('message');
            const subtitleContainer = document.getElementById('subtitleContainer');
            const generateSubtitlesCheckbox = document.getElementById('generateSubtitles');

            messageDiv.textContent = '';
            messageDiv.className = 'message';
            subtitleContainer.innerHTML = ''; // Clear previous subtitles
            currentSubtitles = [];
            currentSubtitleIndex = -1;

            const text = textInput.value.trim();
            const voice = document.getElementById('voiceSelect').value;
            const rate = document.getElementById('rateInput').value;
            const pitch = document.getElementById('pitchInput').value;
            const volume = document.getElementById('volumeInput').value;
            const generateSubtitles = generateSubtitlesCheckbox.checked;

            if (!text) {
                messageDiv.textContent = '请输入要转换的文本。';
                messageDiv.className = 'message error';
                return;
            }

            let body = `text=${encodeURIComponent(text)}`;
            if (voice) {
                body += `&voice=${encodeURIComponent(voice)}`;
            }
            // Always send rate, format with + for positive/zero values
            body += `&rate=${encodeURIComponent((parseInt(rate) >= 0 ? '+' : '') + rate + '%')}`;

            // Always send pitch, format with + for positive/zero values
            body += `&pitch=${encodeURIComponent((parseInt(pitch) >= 0 ? '+' : '') + pitch + 'Hz')}`;

            if (volume !== '100') { // Only send if not default
                // Convert volume to relative percentage for edge-tts
                const volumeChange = parseInt(volume) - 100;
                body += `&volume=${encodeURIComponent((volumeChange >= 0 ? '+' : '') + volumeChange + '%')}`;
            }

            body += `&generateSubtitles=${generateSubtitles}`;

            try {
                const response = await fetch(`${window.location.pathname}synthesize`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                    },
                    body: body,
                });

                if (response.ok) {
                    const data = await response.json();
                    const audioBlob = base64toBlob(data.audioBase64, 'audio/mpeg');
                    const audioUrl = URL.createObjectURL(audioBlob);
                    audioPlayer.src = audioUrl;

                    if (data.subtitles) {
                        console.log('Received subtitles:', data.subtitles); // Debug: Log raw subtitles
                        currentSubtitles = parseVTT(data.subtitles);
                        console.log('Parsed subtitles:', currentSubtitles); // Debug: Log parsed subtitles
                        renderSubtitles(currentSubtitles, subtitleContainer);
                        audioPlayer.ontimeupdate = () => {
                            highlightSubtitle(audioPlayer.currentTime, currentSubtitles, subtitleContainer);
                        };
                    } else {
                        audioPlayer.ontimeupdate = null;
                        console.log('No subtitles received.'); // Debug: Indicate no subtitles
                    }

                    messageDiv.textContent = '语音生成成功!';
                } else {
                    const errorText = await response.text();
                    messageDiv.textContent = `Error: ${response.status} - ${errorText}`;
                    messageDiv.className = 'message error';
                }
            } catch (error) {
                console.error('Fetch error:', error);
                messageDiv.textContent = '生成语音时发生错误。';
                messageDiv.className = 'message error';
            }
        }

        function base64toBlob(base64, mimeType) {
            const byteCharacters = atob(base64);
            const byteNumbers = new Array(byteCharacters.length);
            for (let i = 0; i < byteCharacters.length; i++) {
                byteNumbers[i] = byteCharacters.charCodeAt(i);
            }
            const byteArray = new Uint8Array(byteNumbers);
            return new Blob([byteArray], { type: mimeType });
        }

        function parseVTT(vttContent) {
            const lines = vttContent.split('\n');
            const cues = [];
            let i = 0;
            while (i < lines.length) {
                const line = lines[i].trim();

                if (line === '' || line === 'WEBVTT') {
                    i++;
                    continue;
                }

                // Check if it's a cue identifier (a number)
                if (!isNaN(parseInt(line)) && isFinite(line)) {
                    i++; // Skip the cue identifier
                    if (i >= lines.length) {
                        break;
                    }
                }

                // Now, check for the time range line
                if (i < lines.length && lines[i].includes('-->')) {
                    const timeMatch = lines[i].match(/(\d{2}:\d{2}:\d{2}[,. ]\d{3}) --> (\d{2}:\d{2}:\d{2}[,. ]\d{3})/);
                    if (timeMatch) {
                        const start = timeToSeconds(timeMatch[1]);
                        const end = timeToSeconds(timeMatch[2]);
                        i++; // Move to the next line (text line)
                        let text = '';
                        while (i < lines.length && lines[i].trim() !== '') {
                            text += lines[i].trim() + ' ';
                            i++;
                        }
                        cues.push({ start, end, text: text.trim() });
                    } else {
                        // If it contains '-->' but doesn't match the regex, it's a malformed time line.
                        // Advance `i` to avoid infinite loop.
                        i++;
                    }
                } else {
                    // If it's not a blank line, WEBVTT, cue identifier, or time range,
                    // it's an unexpected line, so just advance `i`.
                    i++;
                }
            }
            return cues;
        }

        function timeToSeconds(timeStr) {
            const parts = timeStr.split(':');
            const secondsAndMs = parts[2].replace(',', '.').split('.'); // Replace comma with dot for consistent parsing
            return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(secondsAndMs[0]) + parseFloat('0.' + secondsAndMs[1]);
        }

        function renderSubtitles(subtitles, container) {
            container.innerHTML = '';
            subtitles.forEach((cue, index) => {
                const p = document.createElement('p');
                p.className = 'subtitle-line';
                p.textContent = cue.text;
                p.dataset.index = index;
                container.appendChild(p);
            });
        }

        function highlightSubtitle(currentTime, subtitles, container) {
            let newIndex = -1;
            for (let i = 0; i < subtitles.length; i++) {
                if (currentTime >= subtitles[i].start && currentTime < subtitles[i].end) {
                    newIndex = i;
                    break;
                }
            }

            if (newIndex !== currentSubtitleIndex) {
                if (currentSubtitleIndex !== -1) {
                    const prevLine = container.querySelector(`[data-index="${currentSubtitleIndex}"]`);
                    if (prevLine) {
                        prevLine.classList.remove('highlight');
                    }
                }
                if (newIndex !== -1) {
                    const currentLine = container.querySelector(`[data-index="${newIndex}"]`);
                    if (currentLine) {
                        currentLine.classList.add('highlight');
                        // Scroll into view
                        const containerHeight = container.clientHeight;
                        const lineHeight = currentLine.offsetHeight;
                        const scrollY = currentLine.offsetTop - container.offsetTop;

                        if (scrollY < container.scrollTop || scrollY + lineHeight > container.scrollTop + containerHeight) {
                            container.scrollTop = scrollY - (containerHeight / 2) + (lineHeight / 2);
                        }
                    }
                }
                currentSubtitleIndex = newIndex;
            }
        }

        // Populate voice options
        const voices = [
            { name: 'zh-CN-XiaoxiaoNeural', display: 'zh-CN-晓晓 (女) - 新闻, 小说, 温和' },
            { name: 'zh-CN-XiaoyiNeural', display: 'zh-CN-晓一 (女) - 卡通, 小说, 活泼' },
            { name: 'zh-CN-YunjianNeural', display: 'zh-CN-云健 (男) - 体育, 小说, 热情' },
            { name: 'zh-CN-YunxiNeural', display: 'zh-CN-云希 (男) - 小说, 活泼, 阳光' },
            { name: 'zh-CN-YunxiaNeural', display: 'zh-CN-云夏 (男) - 卡通, 小说, 可爱' },
            { name: 'zh-CN-YunyangNeural', display: 'zh-CN-云扬 (男) - 新闻, 专业, 可靠' },
            { name: 'zh-CN-liaoning-XiaobeiNeural', display: 'zh-CN-辽宁-小北 (女) - 方言, 幽默' },
            { name: 'zh-CN-shaanxi-XiaoniNeural', display: 'zh-CN-陕西-小妮 (女) - 方言, 开朗' },
            { name: 'zh-HK-HiuGaaiNeural', display: 'zh-HK-晓佳 (女) - 通用, 友好, 积极' },
            { name: 'zh-HK-HiuMaanNeural', display: 'zh-HK-晓曼 (女) - 通用, 友好, 积极' },
            { name: 'zh-HK-WanLungNeural', display: 'zh-HK-文龙 (男) - 通用, 友好, 积极' },
            { name: 'zh-TW-HsiaoChenNeural', display: 'zh-TW-晓辰 (女) - 通用, 友好, 积极' },
            { name: 'zh-TW-HsiaoYuNeural', display: 'zh-TW-晓羽 (女) - 通用, 友好, 积极' },
            { name: 'zh-TW-YunJheNeural', display: 'zh-TW-云哲 (男) - 通用, 友好, 积极' },
            { name: 'zu-ZA-ThandoNeural', display: 'zu-ZA-坦多 (女) - 通用, 友好, 积极' },
            { name: 'zu-ZA-ThembaNeural', display: 'zu-ZA-坦巴 (男) - 通用, 友好, 积极' }
        ];

        const voiceSelect = document.getElementById('voiceSelect');
        const defaultOption = document.createElement('option');
        defaultOption.value = '';
        defaultOption.textContent = '默认';

        voiceSelect.appendChild(defaultOption);

        voices.forEach(voice => {
            const option = document.createElement('option');
            option.value = voice.name;
            option.textContent = voice.display;
            voiceSelect.appendChild(option);
        });

        // Initial display for range inputs
        document.addEventListener('DOMContentLoaded', () => {
            document.getElementById('rateInput').nextElementSibling.value = document.getElementById('rateInput').value + '%';
            document.getElementById('pitchInput').nextElementSibling.value = document.getElementById('pitchInput').value + 'Hz';
            document.getElementById('volumeInput').nextElementSibling.value = document.getElementById('volumeInput').value + '%';
        });
    </script>
</body>
</html>